<?php
namespace DocBoot;

use DI\Attribute\Inject;
use DI\Container;
use DI\ContainerBuilder;
use DI\DependencyException;
use DI\FactoryInterface;
use DI\NotFoundException;
use DocBoot\Controller\ControllerContainer;
use DocBoot\Controller\ControllerContainerBuilder;
use DocBoot\Controller\ExceptionRenderer;
use DocBoot\Controller\HookInterface;
use DocBoot\Controller\ResponseRenderer;
use FastRoute\DataGenerator\GroupCountBased as GroupCountBasedDataGenerator;
use FastRoute\Dispatcher;
use FastRoute\Dispatcher\GroupCountBased as GroupCountBasedDispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std;
use Invoker\Exception\InvocationException;
use Invoker\Exception\NotCallableException;
use Invoker\Exception\NotEnoughParametersException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class  Application implements ContainerInterface, FactoryInterface
{
    /**
     * @return self
     * @throws DependencyException
     * @throws NotFoundException
     */
    static public function createByDefault(array $conf = []): Application
    {
        $default = [
            ResponseRenderer::class => \DI\create(ResponseRenderer::class),
            ExceptionRenderer::class => \DI\create(ExceptionRenderer::class)
        ];
        $builder = new ContainerBuilder();
        $builder->useAttributes(true);
        $builder->addDefinitions($default);
        $builder->addDefinitions($conf);
        $container = $builder->build();
        return $container->make(self::class);
    }

    /**
     * load routes from class
     *
     * @param string $className
     * @param string[] $hooks hook class names
     * @return void
     */
    public function loadRoutesFromClass(string $className, array $hooks=[]): void
    {
        $controller = $this->controllerContainerBuilder->build($className);
        foreach ($controller->getRoutes() as $actionName => $route) {
            $this->routes[] = [
                $route->getMethod(),
                $route->getUri(),
                function (Request $request) use ($className, $actionName, $controller) {
                    $routeInstance = $controller->getRoute($actionName) or abort(new NotFoundHttpException("action $actionName not found"));
                    return ControllerContainer::dispatch($this, $className, $actionName, $routeInstance, $request);
                },
                $hooks
            ];
        }
        $this->controllers[$className] = $controller;
    }

    /**
     * load routes from path
     *
     * 被加载的文件必须以: 类名.php的形式命名
     * @param string $fromPath
     * @param string $namespace
     * @param string[] $hooks
     * @return void
     */
    public function loadRoutesFromPath(string $fromPath, string $namespace = ''): void
    {
        $dir = @dir($fromPath) or abort("dir $fromPath not exist");

        while (!!($entry = $dir->read())) {
            if ($entry == '.' || $entry == '..') {
                continue;
            }
            $path = $fromPath . '/' . str_replace('\\', '/', $entry);
            if (is_file($path) && substr_compare($entry, '.php', strlen($entry) - 4, 4, true) == 0) {
                $class_name = $namespace . '\\' . substr($entry, 0, strlen($entry) - 4);
                $this->loadRoutesFromClass($class_name);
            }
        }
    }

    /**
     * Add route
     * @param string $method
     * @param string $uri
     * @param callable $handler function(Application $demo, Request $request):Response
     * @param string[] $hooks
     */
    public function addRoute($method, $uri, callable $handler, $hooks=[])
    {
        $this->routes[] = [$method, $uri, $handler, $hooks];
    }

    /**
     * @return array
     */
    public function getRoutes()
    {
        return $this->routes;
    }

    /**
     * @return ControllerContainer[]
     */
    public function getControllers(): array
    {
        return $this->controllers;
    }

    /**
     * @return ControllerContainer
     */
    public function getController($className): object
    {
        return $this->controllers[$className] ? : $this->controllerContainerBuilder->build($className);
    }

    /**
     * @param Request|null $request
     * @param bool $send
     * @return Response
     */
    public function dispatch(Request $request = null, $send = true)
    {
        //  TODO 把 Route里的异常处理 ExceptionRenderer 移到这里更妥?
        $renderer = $this->get(ExceptionRenderer::class);
        try{
            if ($request == null) {
                $request = $this->make(Request::class);
            }
            $uri = $request->getRequestUri();
            if (false !== $pos = strpos($uri, '?')) {
                $uri = substr($uri, 0, $pos);
            }
            $uri = rawurldecode($uri);

            $next = function (Request $request)use($uri){
                $dispatcher = $this->getDispatcher();
                $res = $dispatcher->dispatch($request->getMethod(), $uri);
                if ($res[0] == Dispatcher::FOUND) {

                    if (count($res[2])) {
                        $request->attributes->add($res[2]);
                    }
                    list($handler, $hooks) = $res[1];
                    $next = function (Request $request)use($handler){
                        return $handler($request);
                    };
                    foreach (array_reverse($hooks) as $hookName){
                        $next = function($request)use($hookName, $next){
                            $hook = $this->get($hookName);
                            /**@var $hook HookInterface*/
                            return $hook->handle($request, $next);
                        };
                    }
                    return $next($request);

                }elseif ($res[0] == Dispatcher::NOT_FOUND) {
                    abort(new NotFoundHttpException(), [$request->getMethod(), $uri]);
                } elseif ($res[0] == Dispatcher::METHOD_NOT_ALLOWED) {
                    abort(new MethodNotAllowedHttpException($res[1]), [$request->getMethod(), $uri]);
                } else {
                    abort("unknown dispatch return {$res[0]}");
                }
            };

            foreach (array_reverse($this->getGlobalHooks()) as $hookName){
                $next = function($request)use($hookName, $next){
                    $hook = $this->get($hookName);
                    /**@var $hook HookInterface*/
                    return $hook->handle($request, $next);
                };
            }
            $response = $next($request);

            /** @var Response $response */
            if ($send) {
                $response->send();
            }
            return $response;

        }catch (\Exception $e){
            $response = $renderer->render($e);
            if ($send) {
                $response->send();
            }
            return $response;
        }

    }

    /**
     * @return GroupCountBasedDispatcher
     */
    public function getDispatcher()
    {
        $routeCollector = new RouteCollector(new Std(), new GroupCountBasedDataGenerator());
        foreach ($this->routes as $route) {
            list($method, $uri, $handler, $hooks) = $route;
            $uri = $this->getFullUri($uri);
            $routeCollector->addRoute($method, $uri, [$handler, $hooks]);
        }
        return new GroupCountBasedDispatcher($routeCollector->getData());
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     *
     * @return mixed Entry.
     */
    public function get($id)
    {
        return $this->container->get($id);
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has(string $id): bool
    {
        return $this->container->has($id);
    }

    /**
     * Call the given function using the given parameters.
     *
     * @param callable $callable Function to call.
     * @param array $parameters Parameters to use.
     *
     * @return mixed Result of the function.
     *
     * @throws InvocationException Base exception class for all the sub-exceptions below.
     * @throws NotCallableException
     * @throws NotEnoughParametersException
     */
    public function call(callable $callable, array $parameters = array())
    {
        return $this->container->call($callable, $parameters);
    }

    /**
     * @param $name
     * @param array $parameters
     * @return mixed
     * @throws DependencyException
     * @throws NotFoundException
     */
    public function make($name, array $parameters = []) :mixed
    {
        return $this->container->make($name, $parameters);
    }

    /**
     * @param \string[] $globalHooks
     */
    public function setGlobalHooks($globalHooks)
    {
        $this->globalHooks = $globalHooks;
    }

    /**
     * @return \string[]
     */
    public function getGlobalHooks()
    {
        return $this->globalHooks;
    }

    public function getFullUri($uri)
    {
        return rtrim($this->getUriPrefix(), '/').'/'.ltrim($uri, '/');
    }
    /**
     * @return string
     */
    public function getUriPrefix()
    {
        return $this->uriPrefix;
    }

    /**
     * @param string $uriPrefix
     */
    public function setUriPrefix($uriPrefix)
    {
        $this->uriPrefix = $uriPrefix;
    }
    /**
     * @var string
     */
    protected $uriPrefix = '/';

    #[Inject]
    protected Container $container;

    #[Inject]
    protected ControllerContainerBuilder $controllerContainerBuilder;


    /**
     * [
     *      [method, uri, handler, hooks]
     * ]
     * @var array
     */
    protected $routes = [];

    /**
     * @var ControllerContainer[]
     */
    protected array $controllers = [];

    /**
     * @var string[]
     */
    protected $globalHooks = [];

}
